package com.tcy.game.angryrobots;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.SpriteCache;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Matrix4;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.tcy.game.angryrobots.general.CameraHelper;
import com.tcy.game.angryrobots.general.CommonUtils;
import com.tcy.game.angryrobots.general.Config;
import com.tcy.game.angryrobots.general.Particle;
import com.tcy.game.angryrobots.general.ParticleManager;
import com.tcy.game.angryrobots.sprite.BaseShot;
import com.tcy.game.angryrobots.sprite.Captain;
import com.tcy.game.angryrobots.sprite.GameObject;
import com.tcy.game.angryrobots.sprite.Player;
import com.tcy.game.angryrobots.sprite.PlayerShot;
import com.tcy.game.angryrobots.sprite.Robot;

import static com.tcy.game.angryrobots.Assets.*;
import static com.tcy.game.angryrobots.general.MathUtils.*;

/**
 * The <code>WorldView</code> displays the {@link World} on screen. It also provides the means by which the player can control the
 * game.
 * Created by 80002023 on 2016/7/27.
 */
public class WorldView {

    /**
     * The <code>Presenter</code> interface is how the <code>WorldView</code> communicates the state of its controls.
     */
    public static interface Presenter {
        void setController(float x, float y);

        void setFiringController(float dx, float dy);

        void pause();

        void resume();
    }

    private static final float PARTICLE_SIZE = Config.asFloat("particle.size", 0.1875f);

    private static final int SPRITE_CACHE_SIZE = 128; // TODO: add to Config?
    private static final float FIRING_DEAD_ZONE = 0.125f; // TODO: add to Config.
    private static final float JOYSTICK_DISTANCE_MULTIPLIER = 0.2f; // TODO: add to Config.

    private static final int MAX_PARTICLES = 256;

    private final World world;
    private final Presenter presenter;
    private OrthographicCamera worldCamerra;
    private SpriteCache spriteCache;
    private SpriteCache prevSpriteCache;
    private int cacheId;
    private int prevCacheId;
    private Matrix4 prevCacheTransform;
    private Matrix4 cacheTransform;
    private SpriteBatch spriteBatch;
    private Vector3 touchPoint;
    private Vector3 dragPoint;
    private Vector2 joystick;
    private final ParticleManager particleManager;
    private final ParticleAdapter particleAdapter;
    private final FlyupManager flyupManager;
    private final float worldMinX;
    private final float worldMinY;
    private final float worldWidth;
    private final float worldHeight;
    private final float worldMaxX;
    private final float worldMaxY;

    /**
     * Constructs a new WorldView.
     *
     * @param world     the {@link World} that this is a view of.
     * @param presenter the interface by which this <code>WorldView</code> communicates the state of its controls.
     */
    public WorldView(World world, StatusManager statusManager, Presenter presenter) {
        this.world = world;
        this.presenter = presenter;
        Rectangle bounds = world.getRoomBounds();
        worldMinX = bounds.x;
        worldMinY = bounds.y;
        worldWidth = bounds.width;
        worldHeight = bounds.height;
        worldMaxX = worldMinX + worldWidth;
        worldMaxY = worldMinY + worldHeight;
        // TODO: find some way of parameter the viewport mode.
        worldCamerra = CameraHelper.createCamera2(CameraHelper.ViewportMode.PIXEL_PERFECT, width, height, Assets.pixelDensity);
        worldCamerra.update();
        spriteBatch = new SpriteBatch();
        spriteCache = new SpriteCache(SPRITE_CACHE_SIZE, true);
        spriteBatch.setProjectionMatrix(worldCamerra.combined);
        spriteCache.setProjectionMatrix(worldCamerra.combined);
        prevSpriteCache = new SpriteCache(SPRITE_CACHE_SIZE, true);
        prevSpriteCache.setProjectionMatrix(worldCamerra.combined);
        cacheTransform = new Matrix4();
        prevCacheTransform = new Matrix4();
        touchPoint = new Vector3();
        dragPoint = new Vector3();
        joystick = new Vector2();
        particleManager = new ParticleManager(MAX_PARTICLES, PARTICLE_SIZE);
        particleAdapter = new ParticleAdapter(world, particleManager);
        world.addWorldListener(particleAdapter);
        flyupManager = new FlyupManager();
        statusManager.addScoringEventListener(flyupManager);
        resetCaches();
    }

    public void update(float delta) {
        particleAdapter.update(delta);
        flyupManager.update(delta);
    }

    /**
     * Called when the view should be rendered.
     *
     * @param delta the time in seconds since the last render.
     */
    public void render(float delta) {
//        CommonUtils.log("WorldView", "state= " + world.getState());
        switch (world.getState()) {

            case World.RESETTING:
                resetCaches();
                break;

            case World.ENTERED_ROOM:
                if (world.getStateTime() == 0.0f) {
                    createMazeContent();
                    particleAdapter.setRobotColor(world.getRobotColor());
                }
                updatePanning();
                drawWallsAndDoors();
                drawMobiles();
                break;

            case World.PLAYING:
                // TODO: this is really a bit of a hack ... does it really need to be called every tick?
                if (world.getStateTime() == 0.0f) {
                    cacheTransform.idt();
                    spriteCache.setTransformMatrix(cacheTransform);
                    spriteBatch.setTransformMatrix(cacheTransform);
                }
                // break;
            case World.PLAYER_DEAD:
                drawWallsAndDoors();
                drawMobiles();
                break;
        }
    }

    private void resetCaches() {
        cacheId = -1;
        cacheTransform.idt();
        prevCacheTransform.idt();
    }

    private void createMazeContent() {
        cycleCaches();
        cacheId = createWallsAndDoors(spriteCache);
    }

    private void cycleCaches() {
        SpriteCache tempCache = prevSpriteCache;
        prevSpriteCache = spriteCache;
        spriteCache = tempCache;
        prevCacheId = cacheId;
    }

    private int createWallsAndDoors(SpriteCache sc) {
        // Walls and doors never move, so we put them into a sprite cache.
        sc.clear();
        sc.beginCache();
        sc.setColor(Color.BLUE);
        Array<Rectangle> rects = world.getWallRects();
        for (int i = 0; i < rects.size; i++) {
            Rectangle rect = rects.get(i);
            sc.add(Assets.pureWhiteTextureRegion, rect.x + 17, rect.y + 15, rect.width, rect.height);
        }
        sc.setColor(1, 1, 0, 1);
        rects = world.getDoorRects();
        for (int i = 0; i < rects.size; i++) {
            Rectangle rect = rects.get(i);
            sc.add(Assets.pureWhiteTextureRegion, rect.x + 17, rect.y + 15, rect.width, rect.height);
        }
        return sc.endCache();
    }

    private void updatePanning() {
        // If we're moving from one room to another then we want to draw the old room scrolling off as the new room
        // scrolls on.
        final float w = worldWidth;
        final float h = worldHeight;
        CommonUtils.log("WorldView", "updatePanning width: " + w + ", height: " + h);
        float time = min(1.0f, world.getStateTime() / World.ROOM_TRANSITION_TIME);

        // The direction of scrolling is determined by the door position.
        switch (world.getDoorPosition()) {
            case DoorPositions.MIN_Y:
                prevCacheTransform.idt().trn(0.0f, -h * time, 0.0f);
                cacheTransform.idt().trn(0.0f, h - h * time, 0.0f);
                break;

            case DoorPositions.MAX_Y:
                prevCacheTransform.idt().trn(0.0f, h * time, 0.0f);
                cacheTransform.idt().trn(0.0f, -h + h * time, 0.0f);
                break;

            case DoorPositions.MIN_X:
                prevCacheTransform.idt().trn(-w * time, 0.0f, 0.0f);
                cacheTransform.idt().trn(w - w * time, 0.0f, 0.0f);
                break;

            case DoorPositions.MAX_X:
                prevCacheTransform.idt().trn(w * time, 0.0f, 0.0f);
                cacheTransform.idt().trn(-w + w * time, 0.0f, 0.0f);
        }

        prevSpriteCache.setTransformMatrix(prevCacheTransform);
        spriteCache.setTransformMatrix(cacheTransform);
        spriteBatch.setTransformMatrix(cacheTransform);
    }

    private void drawWallsAndDoors() {
        // Draw the old room if it is scrolling off.
        if (world.getState() == World.ENTERED_ROOM && prevCacheId != -1) {
            prevSpriteCache.begin();
            prevSpriteCache.draw(prevCacheId);
            prevSpriteCache.end();
        }

        // Draw the current room.
        spriteCache.begin();
        spriteCache.draw(cacheId);
        spriteCache.end();
    }

    private void drawMobiles() {
        spriteBatch.setProjectionMatrix(worldCamerra.combined);
        spriteBatch.begin();
        spriteBatch.setColor(Color.WHITE);
        drawPlayersShots();
        drawRobotsShots();
        drawRobots();
        drawCaptain();
        drawPlayer();
        drawParticles();
        drawFlyups();
        spriteBatch.end();
    }

    private void drawRobots() {
        spriteBatch.setColor(world.getRobotColor());
        for (Robot robot : world.getRobots()) {
            drawRobot(robot);
        }
        spriteBatch.setColor(Color.WHITE);
    }

    private void drawCaptain() {
        Captain captain = world.getCaptain();
        if (captain.state == Captain.CHASING) {
            drawClipped(captain, Assets.nemesisAnimation.getKeyFrame(captain.stateTime, true));
        }
    }

    private void drawPlayersShots() {
        for (PlayerShot shot : world.getPlayerShots()) {
            draw(shot, Assets.playerShot);
        }
    }

    private void drawRobotsShots() {
        for (BaseShot shot : world.getRobotShots()) {
            draw(shot, Assets.robotShot);
        }
    }

    private void drawPlayer() {
        Player player = world.getPlayer();
        switch (player.state) {
            case Player.WALKING_LEFT:
                draw(player, Assets.playerWalkingLeftAnimation.getKeyFrame(player.stateTime, true));
                break;
            case Player.WALKING_RIGHT:
                draw(player, Assets.playerWalkingRightAnimation.getKeyFrame(player.stateTime, true));
                break;
            case Player.FACING_LEFT:
                draw(player, Assets.playerWalkingLeft2);
                break;
            case Player.FACING_RIGHT:
                draw(player, Assets.playerWalkingRight2);
                break;
        }
    }

    private void drawRobot(Robot robot) {
        Animation robotAnimation = null;
        switch (robot.state) {
            case Robot.SCANNING:
                robotAnimation = Assets.robotScanningAnimation;
                break;
            case Robot.WALKING_DOWN:
                robotAnimation = Assets.robotWalkingLeftAnimation;
                break;
            case Robot.WALKING_LEFT:
                robotAnimation = Assets.robotWalkingLeftAnimation;
                break;
            case Robot.WALKING_LEFT_DIAGONAL:
                robotAnimation = Assets.robotWalkingLeftAnimation;
                break;
            case Robot.WALKING_RIGHT:
                robotAnimation = Assets.robotWalkingRightAnimation;
                break;
            case Robot.WALKING_RIGHT_DIAGONAL:
                robotAnimation = Assets.robotWalkingRightAnimation;
                break;
            case Robot.WALKING_UP:
                robotAnimation = Assets.robotWalkingRightAnimation;
                break;
            default:
                robotAnimation = Assets.robotScanningAnimation;
        }
        draw(robot, robotAnimation.getKeyFrame(robot.stateTime, true));
    }

    private void draw(GameObject go, TextureRegion region) {
        spriteBatch.draw(region, go.x + 17, go.y + 15, go.width, go.height);
    }

    private void drawClipped(GameObject go, TextureRegion region) {
        float boundsMinX = worldMinX + World.OUTER_WALL_ADJUST + World.WALL_HEIGHT;
        float boundsMaxX = worldMaxX - World.OUTER_WALL_ADJUST - World.WALL_HEIGHT;
        float boundsMinY = worldMinY + World.OUTER_WALL_ADJUST + World.WALL_HEIGHT;
        float boundsMaxY = worldMaxY - World.OUTER_WALL_ADJUST - World.WALL_HEIGHT;

        // Don't draw if it's completely out of bounds.
        float maxX = go.x + go.width;
        if (maxX < boundsMinX) return;
        float minX = go.x;
        if (minX > boundsMaxX) return;
        float maxY = go.y + go.height;
        if (maxY < boundsMinY) return;
        float minY = go.y;
        if (minY > boundsMaxY) return;

        // Clip to the visible bounds.
        float x = go.x;
        float y = go.y;
        int srcX = region.getRegionX();
        int srcY = region.getRegionY();
        int srcWidth = region.getRegionWidth();
        int srcHeight = region.getRegionHeight();
        if (minX < boundsMinX) {
            float n = (boundsMinX - minX);
            x += n;
            n *= (srcWidth / go.width);
            srcX += n;
            srcWidth -= n;
        } else if (maxX > boundsMaxX) {
            float n = (maxX - boundsMaxX);
            srcWidth -= n * (srcWidth / go.width);
        }
        if (minY < boundsMinY) {
            float n = (boundsMinY - minY);
            y += n;
            srcHeight -= n * (srcHeight / go.height);
        } else if (maxY > boundsMaxY) {
            float n = (maxY - boundsMaxY) * (srcHeight / go.height);
            srcHeight -= n;
            srcY += n;
        }
        float width = go.width * srcWidth / region.getRegionWidth();
        float height = go.height * srcHeight / region.getRegionHeight();

        spriteBatch.draw(region.getTexture(), x, y, width, height, srcX, srcY, srcWidth, srcHeight, false, false);
    }

    private void drawParticles() {
        spriteBatch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE);
        for (Particle particle : particleManager.getParticles()) {
            if (particle.active) {
                spriteBatch.setColor(particle.color);
                spriteBatch.draw(Assets.pureWhiteTextureRegion, particle.x, particle.y, particle.size, particle.size);
            }
        }
        spriteBatch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
    }

    private void drawFlyups() {
        BitmapFont font = Assets.flyupFont;
        float scale = font.getScaleX();
//        font.setScale(1.0f / Assets.pixelDensity);
        spriteBatch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE);
        for (Flyup flyup : flyupManager.flyups) {
            if (flyup.active) {
                font.draw(spriteBatch, flyup.scoreString, flyup.x, flyup.y);
            }
        }
        spriteBatch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
//        font.setScale(scale);
    }

    /**
     * Updates the state of the on-screen controls.
     *
     * @param delta time in seconds since the last render.
     */
    public void updateControls(float delta) {
        presenter.setController(0.0f, 0.0f);
        if (Gdx.input.justTouched()) {
            worldCamerra.unproject(touchPoint.set(Gdx.input.getX(), Gdx.input.getY(), 0));
            if (world.isPaused()) {
                presenter.resume();
            } else if (touchPoint.y >= worldMaxY) {
                presenter.pause();
            }
        } else if (Gdx.input.isTouched()) {
            worldCamerra.unproject(dragPoint.set(Gdx.input.getX(), Gdx.input.getY(), 0));
            float dx = dragPoint.x - touchPoint.x;
            float dy = dragPoint.y - touchPoint.y;
            joystick.set(dx, dy).scl(JOYSTICK_DISTANCE_MULTIPLIER);
            float len = joystick.len();
            if (len > 1) {
                joystick.nor();
            }
            if (presenter != null) {
                presenter.setController(joystick.x, joystick.y);
                if (len >= FIRING_DEAD_ZONE) {
                    joystick.nor();
                    presenter.setFiringController(joystick.x, joystick.y);
                }
            }
        }
    }
}
